/*
 * Copyright 2009, Team Mondegreen:
 * Brian Hsieh - bh2181@columbia.edu
 * Eugene Yao - eyy2102@columbia.edu
 * Jake Amonette - jaa2149@columbia.edu
 * Tal Giat - tg2235@colubia.edu
 * 
 * This file is part of AMFM (A Minimal Fractal Music Language), a language created for
 * fractal music as part of the Programming Languages and Translator class at Columbia university.
 *
 * For more details go to: http://code.google.com/p/amfm/
 * 
 * AMFM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * at your option any later version.
 * 
 * AMFM is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with AMFM.  If not, see <http://www.gnu.org/licenses/>. 
 */

package com.myronmarston.music;

import java.io.File;
import java.io.IOException;

import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequence;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.Track;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;

import com.sun.media.sound.AudioSynthesizer;

/**
 * This class is used to create audio WAV files.
 * 
 * @author Jake
 */
public class AMFMAudioFileCreator {
  private static final double MICROSECONDS_PER_SECOND = 1000000.0;

  private final AMFMOutputManager outputManager;

  /**
   * Constructor.
   * 
   * @param outputManager the output manager containing the midi sequence to convert to a wav file.
   */
  public AMFMAudioFileCreator(AMFMOutputManager outputManager) {
    this.outputManager = outputManager;
  }

  /**
   * Converts the output to a wav file and saves it to disk.
   * 
   * @param wavFileName the file name to save to
   * @throws javax.sound.midi.MidiUnavailableException if there is a midi error
   * @throws java.io.IOException if there is an I/O error
   */
  protected void saveWavFile(final String wavFileName) throws MidiUnavailableException, IOException {
    AudioSynthesizer synth = null;
    AudioInputStream stream1 = null;
    AudioInputStream stream2 = null;
    try {
      synth = AMFMAudioFileCreator.getAudioSynthesizer();
      synth.loadAllInstruments(MidiSoundbank.getCurrent().getSoundbank());

      // Open AudioStream from AudioSynthesizer with default values
      stream1 = synth.openStream(null, null);

      // Play Sequence into AudioSynthesizer Receiver.
      double totalLength = this.sendOutputSequenceMidiEvents(synth.getReceiver());

      // Calculate how long the WAVE file needs to be.
      long len = (long) (stream1.getFormat().getFrameRate() * totalLength);
      stream2 = new AudioInputStream(stream1, stream1.getFormat(), len);
      
      // Write the wave file to disk
      AudioSystem.write(stream2, AudioFileFormat.Type.WAVE, new File(wavFileName));
    } finally {
      if (stream1 != null)
        stream1.close();
      if (stream2 != null)
        stream2.close();
      if (synth != null)
        synth.close();
    }
  }

  /**
   * Gets the audio synthesizer.
   * 
   * @return the audio synthesizer
   * @throws javax.sound.midi.MidiUnavailableException if the audio synthesizer cannot be found
   */
  protected static AudioSynthesizer getAudioSynthesizer() throws MidiUnavailableException {
    // First check if default synthesizer is AudioSynthesizer.
    Synthesizer synth = MidiSystem.getSynthesizer();
    if (synth instanceof AudioSynthesizer)
      return (AudioSynthesizer) synth;

    // now check the others...
    for (MidiDevice.Info info : MidiSystem.getMidiDeviceInfo()) {
      MidiDevice device = MidiSystem.getMidiDevice(info);
      if (device instanceof AudioSynthesizer)
        return (AudioSynthesizer) device;
    }

    throw new MidiUnavailableException("The AudioSynthesizer is not available.");
  }

  /**
   * Sends the outputManager's midi sequence events to the given receiver.
   * 
   * @param receiver the receiver
   * @return the length of the audio, in seconds
   */
  private double sendOutputSequenceMidiEvents(Receiver receiver) {
    Sequence sequence = this.outputManager.getSequence();
    // this method is only designed to handle the PPQ division type.
    assert sequence.getDivisionType() == Sequence.PPQ : sequence.getDivisionType();

    int microsecondsPerQtrNote = 60000000 / 90; // Tempo.convertToMicrosecondsPerQuarterNote(this.outputManager.getFractalPiece().getTempo());
    int seqRes = sequence.getResolution();
    long totalTime = 0;

    for (Track track : sequence.getTracks()) {
      long lastTick = 0;
      long curTime = 0;

      for (int i = 0; i < track.size(); i++) {
        MidiEvent event = track.get(i);
        long tick = event.getTick();
        curTime += ((tick - lastTick) * microsecondsPerQtrNote) / seqRes;
        lastTick = tick;
        MidiMessage msg = event.getMessage();
        if (!(msg instanceof MetaMessage)) {
          receiver.send(msg, curTime);
        }
      }

      // make the total time be the time of the langest track
      totalTime = Math.max(curTime, totalTime);
    }

    return totalTime / MICROSECONDS_PER_SECOND;
  }
}
